feat: readyz: make the overall wait timeout configurable per template - #487
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
62b205b to
a72632b
Compare
|
Maya Wang (@mayawang) - I have recently added substrate/internal/readyz/readyz.go Line 42 in 0f6144e Have you considered to extend the readyz with a timeout via actorTemplate and push it up to the atelet? |
Thanks Dmitry Berkovich (@dberkov) — agreed, readyz is the better mechanism. One note on how it fits with this PR: Strong +1 on making the timeout configurable — and it's live for us, not hypothetical. The Hermes actor we're onboarding declares readyz ( The failure mode is harsher than the timer's, too. A WaitAll error propagates out of RunWorkload/RestoreWorkload ( Shape I'd propose: optional I'd keep it as a separate PR rather than folding it in — this one is internal-only, and adding a v1alpha1 field pulls in codegen and API review. The two don't overlap, so they can land in either order. Happy to take it since I have the workload to validate against, unless you'd rather own it as your API — either way I'd want your input on the field bounds. |
6a8d224 to
cff5609
Compare
|
Dmitry Berkovich (@dberkov) — this has been reshaped since your review, so rather than have you re-read Two of the four knobs are gone, not rebased. Request parking landed on main and covers that ground properly: the resume timeout is now The two survivors moved from env vars to flags, matching the convention parking established. One correction to my comment abve: The runsc opt-out became an automatic capability probe, and that turned up a real bug worth flagging since it's the one change with no e2e coverage. The probe originally shelled Rebased onto current main, so this is now post-atunnel. Relevant to the route timeout: it attaches to the
|
cff5609 to
ee2163b
Compare
The 30s readiness deadline was a package constant, so a workload that legitimately takes longer to bind its HTTP server could not be accommodated without raising the ceiling for every actor in the cluster. How long a workload takes to become ready is a property of that workload, so this plumbs a per-template timeout through the existing readyz chain: ContainerReadyz.timeoutSeconds -> ateletpb.Readyz -> ateompb.Readyz -> readyz.Wait. Unset means the ateom's default, which is the renamed DefaultOverallTimeout, still 30s. Zero is not a meaningful deadline here -- unlike a warmup delay, a zero timeout could never be met -- so a non-positive value on the wire is read as "unset" and falls back to the default. The CRD field is a pointer with Minimum=1 so the API rejects it outright rather than silently substituting. No readyz.WaitAll call site changes: the timeout rides on the probe. The e2e probe fixture now declares a readyz probe with a non-default timeoutSeconds. That gives the readyz path its first e2e coverage and exercises the value crossing ateapi -> atelet -> ateom on real binaries, on both the run and restore paths.
ee2163b to
45f90d8
Compare
Dmitry Berkovich (dberkov)
left a comment
There was a problem hiding this comment.
2 minor comments, otherwise LGTM
Per review: declare the 30s default with +kubebuilder:default=30 so it is visible on the stored object and in `kubectl explain`, rather than being a constant an author has to know the ateom applies. With the API server defaulting the field, the nil check in toAteletReadyz is no longer load-bearing and becomes a plain deref. It stays nil-safe: the conversion is also reachable with an in-process template that never went through admission, and zero on the wire already means the same 30s. envtest now asserts the read-back value is 30, so the default is checked against a real API server rather than only the marker.
Per review, following the API convention for CRDs: when the zero value is not valid, a pointer buys nothing. Minimum=1 rejects an explicit 0 before any controller sees the object, and +kubebuilder:default=30 fills in the rest, so the field always holds valid data and the conversion is a plain assignment rather than a deref behind a nil check. Also folds the readyz defaulting assertions into TestActorTemplateValidation alongside the other readyz cases, via an optional verify hook for cases that check what the API server stored rather than whether it accepted the create. The standalone path-default test goes away with it. The explicit-zero case goes too: omitempty makes 0 indistinguishable from unset through a typed client, so it now defaults to 30 rather than being rejected. The -1 case still covers the Minimum=1 bound that rejects a manifest spelling out 0.
With TimeoutSeconds a plain int32, toAteletReadyz assigns it unconditionally, so a container whose probe omits the timeout no longer exercises a distinct path through the conversion.
Thanks — all addressed, PTAL. One consequence worth flagging: with omitempty on a non-pointer, a Go client that sets 0 doesn't serialize the field, so it's read as unset and defaults to 30 rather than being rejected. A manifest that spells out 0 still hits Minimum=1. I dropped the envtest case for explicit zero since it can't be expressed through the typed client; the -1 case still covers the bound. |
#714) > Split out of #487, which bundled three unrelated changes. ## Summary Envoy's end-to-end timeout on the workload route is hardcoded at `10s` in `buildRoutes`. An actor that legitimately holds a request open longer gets cut off: a harness relaying an LLM completion keeps the request open for the whole generation, and the client sees a **504 mid-turn**. Adds `--route-timeout` on atenet-router, and pairs it with a route-level `idle_timeout` so the ceiling is actually reachable. **The default is 10s, so behavior is unchanged** unless an operator passes the flag. ## Why the route timeout alone was not enough Raised in review by @LiorLieberman and @yan-vlasov, and they were right — the first version of this PR did not do what it claimed. We never set `stream_idle_timeout` on the HTTP connection manager, so Envoy applies its default of **5 minutes**. Per the HCM proto, that default is "overridable by the route-level `idle_timeout`", and when it fires "the stream is terminated with a 408 Request Timeout error code if no upstream response header has been received, otherwise a stream reset occurs." That is exactly this PR's case. A turn relaying a non-streaming completion sends no bytes at all while the actor is thinking, and a request parked across a suspend/resume is idle by the same measure. Both are progressing; Envoy cannot tell. So `--route-timeout=30m` would still have been cut at 5 minutes with a 408 — the knob would have looked like it worked and silently not. `routeIdleTimeout()` therefore resolves the accompanying idle timeout as `max(routeTimeout, 5m)`. Taking the larger keeps the operator's ceiling honest without ever making the idle timer *stricter* than it is today: below 5 minutes the route timeout fires first regardless, so at the 10s default this is a no-op. Route-level rather than HCM-level, so it stays scoped to workload traffic instead of every stream through the router. It is derived rather than exposed as a second `--route-idle-timeout` flag so the two cannot drift apart, with one silently defeating the other — happy to make it explicit if reviewers prefer. For naming: what this PR sets is the route-level `timeout`, which bounds upstream response time. Envoy's HCM `request_timeout` bounds how long the *request* takes to be received, which is not the limit in question here. ## Changes `cmd/atenet/internal/router/` — adds `XdsServer.routeTimeout` with a `SetRouteTimeout` setter and a `defaultRouteTimeout` const, wired from `routerConfig.RouteTimeout` / `--route-timeout`. Same shape as the adjacent `SetExtProcMessageTimeout` and `SetExtProcMaxRequests`, and a flag on the existing config struct rather than an env read, matching the convention the parked-request work established. Wired in `startEnvoyDataplane`. Adds `envoyDefaultStreamIdleTimeout` (5m) and `routeIdleTimeout()`, applied as the route's `IdleTimeout` in `buildRoutes`. A non-positive value leaves the default in place, since Envoy reads a zero route timeout as *no timeout at all*. The knob bounds the actor's own handling time only. The resume that may precede a request is covered by request parking and the ext_proc message timeout, both of which already derive from `--parked-request-budget`. `manifests/ate-install/atenet-router.yaml` documents it as a commented-out entry. ## Verification - `go build ./...`, `go vet ./...`, `go test ./...` — all pass. - `xds_test.go` reads the timeout back out of `buildRoutes`, where Envoy actually picks it up: default, setter override, and non-positive-keeps-default. The helper pins that route to `OriginalDstClusterName` — a change that moved actor traffic onto some other route would otherwise leave the test passing while the timeout governed a route nothing uses. - Two added subtests cover the pairing: `IdleTimeoutTracksLongerRouteTimeout` and `IdleTimeoutKeepsEnvoyDefaultWhenRouteTimeoutIsShorter`. - **On a live GKE cluster**, read back out of Envoy's own `/config_dump`. With the new image and no flag, the workload route reports `timeout: 10s`, so the default is genuinely unchanged. With `--route-timeout=5m` it reports `timeout: 300s`. Same binary, same manifest, only the flag differs. Caveat on that measurement: it was taken before `ingress: route actor ingress through the atunnel mTLS server` landed, so the route it read was the old `dynamic_forward_proxy` path to pod-IP:80. After rebasing, the timeout attaches to the `actor_original_dst` route that replaced it — which is now pinned by the test above rather than left to inspection. The `idle_timeout` pairing has test coverage only, not a live `/config_dump` read. - **Regression, resume with parking on the path:** a conversation actor that had been suspended for 4 days was resumed by an ordinary request through the router — HTTP 200 in 3.74s, exactly one parked request, `parking_wait_duration_seconds{outcome="served"} = 3.459s`, no shed and no `budget_exhausted`. ## Follow-up Per-ActorTemplate (or per-request) configurability, raised by @ronlv10: agreed it needs an API and is follow-up shaped rather than something to fold in here. The global flag remains useful as the cluster-wide ceiling. ## Relationship to #465 This is a stopgap for the connected-socket suspend/restore problem tracked in **#465 (suspend-safe actor networking)**. Once actor network traffic survives checkpoint/restore natively, much of the need to raise this ceiling should go away; this just makes the current behavior tunable in the meantime. Fixes #<issue_number_goes_here> > It's a good idea to open an issue first for discussion. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR --------- Co-authored-by: Maya Wang <mymaya@google.com>
Summary
readyz.Waitpolls until the container returns 200 or a hardcoded 30selapses. A workload that legitimately takes longer to bind its HTTP server
cannot be accommodated without raising the ceiling for every actor in the
cluster, and losing that race fails the actor start.
How long a workload takes to become ready is a property of that workload, so
this makes the deadline a per-template setting rather than a package constant.
Adds optional
timeoutSecondstoContainerReadyz. Unset keeps today's 30s,so no existing template changes behavior.
Changes
The value rides on the existing probe, so it follows the chain the probe already
takes and no call site needs to know about it:
ContainerReadyz.timeoutSeconds→toAteletReadyz→ateletpb.Readyz→toAteomReadyz→ateompb.Readyz→readyz.Waitpkg/api/v1alpha1/actortemplate_types.go—TimeoutSeconds *int32,+optional,Minimum=1,Maximum=3600.internal/proto/ateletpb/atelet.proto,internal/proto/ateompb/ateom.proto—int32 timeout_seconds = 2on bothReadyzmessages.cmd/ateapi/internal/controlapi/workload_spec.go,cmd/atelet/main.go— passit through the two conversions.
internal/readyz/readyz.go—OverallTimeoutbecomesDefaultOverallTimeout(still 30s) andWaitresolves its deadline through anew
overallTimeout(probe)helper..pb.go,zz_generated.deepcopy.go, and theactortemplatesCRD.None of the four
readyz.WaitAllcall sites change.On the zero value. Unlike a warmup delay — where zero is a real request
meaning "checkpoint immediately" — a zero readiness deadline could never be met,
so it is never something a template author means. A non-positive value on the
wire is therefore read as "unset" and falls back to the default, and the CRD
field is a pointer with
Minimum=1so the API rejects0outright rather thansilently substituting 30s behind the author's back.
On bounding, which was the open question left on the previous revision:
bounded at
3600. A template asking to wait longer than an hour for readinessis expressing a broken workload, not a slow one, and the bound keeps a typo from
pinning a worker for a day.
Verification
go build ./...,go vet ./...,gofmt,go test ./...— all pass.internal/readyz/readyz_test.go—overallTimeoutresolves unset andnegative to the default and honors an explicit value;
Waitagainst a portnothing binds gives up at the probe's 1s deadline rather than the 30s default.
workload_spec_test.go,cmd/atelet/main_test.go— the timeout crosses bothconversions, and a probe without one stays zero on the wire.
actortemplate_validation_test.go— the bounds are enforced by a real APIserver. This suite runs under envtest against the generated CRD directory, so
it exercises the regenerated
actortemplatesCRD rather than the Go markers:300is accepted, unset is accepted, and0,-1and3601are allrejected by apiserver schema validation.
On a real cluster, via CI.
internal/e2e/fixtures/probenow declares areadyzprobe withtimeoutSeconds: 60, pointed at the/healthzthe probebinary already serves on
:80. The kind e2e that runs on every PR thereforeexercises the value crossing ateapi → atelet → ateom on real binaries, across
the auth matrix, on both the run and restore paths. This is also the readyz
path's first e2e coverage — no fixture declared a probe before.
Wire compatibility degrades safely in both skew directions:
timeout_secondsisa new field 2 on a
Readyzmessage that has only ever had field 1, so an oldateom ignores it and an old ateapi leaves it zero, which reads as the 30s
default.
No GKE run. What that would add over the above is a workload whose readiness
genuinely exceeds 30s, and that is the readiness-sidecar work rather than this
PR.
Fixes #<issue_number_goes_here>